﻿// NOTE: This comes from http://www.codeproject.com/Articles/5501/The-Multimedia-Timer-for-the-NET-Framework.

namespace MyCompany.Core {
	using System;
	using System.ComponentModel;
	using System.Diagnostics;
	using System.Runtime.InteropServices;

	#region License

	/* Copyright (c) 2006 Leslie Sanford
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy 
 * of this software and associated documentation files (the "Software"), to 
 * deal in the Software without restriction, including without limitation the 
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 
 * sell copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software. 
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
 * THE SOFTWARE.
 */

	#endregion

	#region Contact

	/*
 * Leslie Sanford
 * Email: jabberdabber@hotmail.com
 */

	#endregion

	/// <summary>
	///     Represents the Windows multimedia timer.
	/// </summary>
	public sealed class MultimediaTimer : IComponent {
		/// <summary>
		///     Defines constants for the multimedia MultimediaTimer's event types.
		/// </summary>
		public enum TimerMode {
			/// <summary>
			///     MultimediaTimer event occurs once.
			/// </summary>
			OneShot,

			/// <summary>
			///     MultimediaTimer event occurs periodically.
			/// </summary>
			Periodic
		};

		// Represents the method that is called by Windows when a timer event occurs.

		// Indicates that the operation was successful.
		const int TIMERR_NOERROR = 0;

		static readonly TimerCaps caps;

		volatile bool disposed;

		// MultimediaTimer identifier.

		// MultimediaTimer mode.
		volatile TimerMode mode;

		// Period between timer events in milliseconds.
		volatile int period;

		// MultimediaTimer resolution in milliseconds.
		volatile int resolution;

		// Called by Windows when a timer periodic event occurs.

		// Indicates whether or not the timer is running.

		// Indicates whether or not the timer has been disposed.

		// The ISynchronizeInvoke object to use for marshaling events.
		ISynchronizeInvoke synchronizingObject;

		EventRaiser tickRaiser;

		TimeProc timeProcOneShot;

		TimeProc timeProcPeriodic;

		int timerID;

		// For implementing IComponent.

		// Multimedia timer capabilities.

		/// <summary>
		///     Initialize class.
		/// </summary>
		static MultimediaTimer() {
			// Get multimedia timer capabilities.
			timeGetDevCaps(ref caps, Marshal.SizeOf(caps));
		}

		/// <summary>
		///     Initializes a new instance of the MultimediaTimer class with the specified IContainer.
		/// </summary>
		/// <param name="container">
		///     The IContainer to which the MultimediaTimer will add itself.
		/// </param>
		public MultimediaTimer(IContainer container) {
			// Required for Windows.Forms Class Composition Designer support
			container.Add(this);

			Initialize();
		}

		/// <summary>
		///     Initializes a new instance of the MultimediaTimer class.
		/// </summary>
		public MultimediaTimer() {
			Initialize();
		}

		/// <summary>
		///     Gets the timer capabilities.
		/// </summary>
		public static TimerCaps Capabilities
		{
			get
			{
				return caps;
			}
		}

		/// <summary>
		///     Gets a value indicating whether the MultimediaTimer is running.
		/// </summary>
		public bool IsRunning { get; set; }

		/// <summary>
		///     Gets the timer mode.
		/// </summary>
		/// <exception cref="ObjectDisposedException">
		///     If the timer has already been disposed.
		/// </exception>
		public TimerMode Mode
		{
			get
			{
				#region Require

				if (disposed)
					throw new ObjectDisposedException("MultimediaTimer");

				#endregion

				return mode;
			}
			set
			{
				#region Require

				if (disposed)
					throw new ObjectDisposedException("MultimediaTimer");

				#endregion

				mode = value;

				if (IsRunning) {
					Stop();
					Start();
				}
			}
		}

		/// <summary>
		///     Gets or sets the time between Tick events.
		/// </summary>
		/// <exception cref="ObjectDisposedException">
		///     If the timer has already been disposed.
		/// </exception>
		public int Period
		{
			get
			{
				#region Require

				if (disposed)
					throw new ObjectDisposedException("MultimediaTimer");

				#endregion

				return period;
			}
			set
			{
				#region Require

				if (disposed)
					throw new ObjectDisposedException("MultimediaTimer");
				if (value < Capabilities.periodMin || value > Capabilities.periodMax)
					throw new ArgumentOutOfRangeException("Period", value, "Multimedia MultimediaTimer period out of range.");

				#endregion

				period = value;

				if (IsRunning) {
					Stop();
					Start();
				}
			}
		}

		/// <summary>
		///     Gets or sets the timer resolution.
		/// </summary>
		/// <exception cref="ObjectDisposedException">
		///     If the timer has already been disposed.
		/// </exception>
		/// <remarks>
		///     The resolution is in milliseconds. The resolution increases
		///     with smaller values; a resolution of 0 indicates periodic events
		///     should occur with the greatest possible accuracy. To reduce system
		///     overhead, however, you should use the maximum value appropriate
		///     for your application.
		/// </remarks>
		public int Resolution
		{
			get
			{
				#region Require

				if (disposed)
					throw new ObjectDisposedException("MultimediaTimer");

				#endregion

				return resolution;
			}
			set
			{
				#region Require

				if (disposed)
					throw new ObjectDisposedException("MultimediaTimer");
				if (value < 0)
					throw new ArgumentOutOfRangeException("Resolution", value, "Multimedia timer resolution out of range.");

				#endregion

				resolution = value;

				if (IsRunning) {
					Stop();
					Start();
				}
			}
		}

		/// <summary>
		///     Gets or sets the object used to marshal event-handler calls.
		/// </summary>
		public ISynchronizeInvoke SynchronizingObject
		{
			get
			{
				#region Require

				if (disposed)
					throw new ObjectDisposedException("MultimediaTimer");

				#endregion

				return synchronizingObject;
			}
			set
			{
				#region Require

				if (disposed)
					throw new ObjectDisposedException("MultimediaTimer");

				#endregion

				synchronizingObject = value;
			}
		}

		public event EventHandler Disposed;

		/// <summary>
		///     Frees timer resources.
		/// </summary>
		public void Dispose() {
			#region Guard

			if (disposed)
				return;

			#endregion

			if (IsRunning)
				Stop();

			disposed = true;

			OnDisposed(EventArgs.Empty);
		}

		public ISite Site { get; set; }

		/// <summary>
		///     Occurs when the MultimediaTimer has started;
		/// </summary>
		public event EventHandler Started;

		/// <summary>
		///     Occurs when the MultimediaTimer has stopped;
		/// </summary>
		public event EventHandler Stopped;

		/// <summary>
		///     Occurs when the time period has elapsed.
		/// </summary>
		public event EventHandler Tick;

		~MultimediaTimer() {
			if (IsRunning) // Stop and destroy timer.
				timeKillEvent(timerID);
		}

		void OnDisposed(EventArgs e) {
			EventHandler handler = Disposed;

			if (handler != null)
				handler(this, e);
		}

		// Initialize timer with default values.

		/// <summary>
		///     Starts the timer.
		/// </summary>
		/// <exception cref="ObjectDisposedException">
		///     The timer has already been disposed.
		/// </exception>
		/// <exception cref="TimerStartException">
		///     The timer failed to start.
		/// </exception>
		public void Start() {
			#region Require

			if (disposed)
				throw new ObjectDisposedException("MultimediaTimer");

			#endregion

			#region Guard

			if (IsRunning)
				return;

			#endregion

			// If the periodic event callback should be used.
			if (Mode == TimerMode.Periodic) // Create and start timer.
				timerID = timeSetEvent(Period, Resolution, timeProcPeriodic, 0, (int)Mode);
			// Else the one shot event callback should be used.
			else
			// Create and start timer.
				timerID = timeSetEvent(Period, Resolution, timeProcOneShot, 0, (int)Mode);

			// If the timer was created successfully.
			if (timerID != 0) {
				IsRunning = true;

				if (SynchronizingObject != null && SynchronizingObject.InvokeRequired)
					SynchronizingObject.BeginInvoke(new EventRaiser(OnStarted), new object[] { EventArgs.Empty });
				else
					OnStarted(EventArgs.Empty);
			}
			else
				throw new TimerStartException("Unable to start multimedia MultimediaTimer.");
		}

		/// <summary>
		///     Stops timer.
		/// </summary>
		/// <exception cref="ObjectDisposedException">
		///     If the timer has already been disposed.
		/// </exception>
		public void Stop() {
			#region Require

			if (disposed)
				throw new ObjectDisposedException("MultimediaTimer");

			#endregion

			#region Guard

			if (!IsRunning)
				return;

			#endregion

			// Stop and destroy timer.
			int result = timeKillEvent(timerID);

			Debug.Assert(result == TIMERR_NOERROR);

			IsRunning = false;

			if (SynchronizingObject != null && SynchronizingObject.InvokeRequired)
				SynchronizingObject.BeginInvoke(new EventRaiser(OnStopped), new object[] { EventArgs.Empty });
			else
				OnStopped(EventArgs.Empty);
		}

		[DllImport("winmm.dll")]
		static extern int timeGetDevCaps(ref TimerCaps caps, int sizeOfTimerCaps);

		[DllImport("winmm.dll")]
		static extern int timeKillEvent(int id);

		[DllImport("winmm.dll")]
		static extern int timeSetEvent(int delay, int resolution, TimeProc proc, int user, int mode);

		void Initialize() {
			mode = TimerMode.Periodic;
			period = Capabilities.periodMin;
			resolution = 1;

			IsRunning = false;

			timeProcPeriodic = TimerPeriodicEventCallback;
			timeProcOneShot = TimerOneShotEventCallback;
			tickRaiser = OnTick;
		}

		// Callback method called by the Win32 multimedia timer when a timer
		// periodic event occurs.

		// Raises the Started event.
		void OnStarted(EventArgs e) {
			EventHandler handler = Started;

			if (handler != null)
				handler(this, e);
		}

		// Raises the Stopped event.
		void OnStopped(EventArgs e) {
			EventHandler handler = Stopped;

			if (handler != null)
				handler(this, e);
		}

		// Raises the Tick event.
		void OnTick(EventArgs e) {
			EventHandler handler = Tick;

			if (handler != null)
				handler(this, e);
		}

		void TimerOneShotEventCallback(int id, int msg, int user, int param1, int param2) {
			if (synchronizingObject != null) {
				synchronizingObject.BeginInvoke(tickRaiser, new object[] { EventArgs.Empty });
				Stop();
			}
			else {
				OnTick(EventArgs.Empty);
				Stop();
			}
		}

		void TimerPeriodicEventCallback(int id, int msg, int user, int param1, int param2) {
			if (synchronizingObject != null)
				synchronizingObject.BeginInvoke(tickRaiser, new object[] { EventArgs.Empty });
			else
				OnTick(EventArgs.Empty);
		}

		/// <summary>
		///     Represents information about the multimedia MultimediaTimer's capabilities.
		/// </summary>
		[StructLayout(LayoutKind.Sequential)]
		public struct TimerCaps {
			/// <summary>
			///     Minimum supported period in milliseconds.
			/// </summary>
			public int periodMin;

			/// <summary>
			///     Maximum supported period in milliseconds.
			/// </summary>
			public int periodMax;
		}

		/// <summary>
		///     The exception that is thrown when a timer fails to start.
		/// </summary>
		public class TimerStartException : ApplicationException {
			/// <summary>
			///     Initializes a new instance of the TimerStartException class.
			/// </summary>
			/// <param name="message">
			///     The error message that explains the reason for the exception.
			/// </param>
			public TimerStartException(string message) : base(message) {}
		}

		delegate void EventRaiser(EventArgs e);

		delegate void TimeProc(int id, int msg, int user, int param1, int param2);
	}
}
